Node.js 프로세스 종료 전략: 신호, 오류 및 Graceful 셧다운
Emily Parker
Product Engineer · Leapcell

배경 소개
서비스가 배포된 후에는 런타임 환경(컨테이너, PM2 등)에 의해 스케줄링되거나, 서비스 업그레이드로 인해 재시작되거나, 프로세스 충돌을 일으키는 다양한 예외가 발생하는 것이 불가피합니다. 일반적으로 런타임 환경에는 서비스 프로세스에 대한 상태 모니터링 메커니즘이 있습니다. 프로세스가 충돌하면 런타임이 다시 시작합니다. 업그레이드 중에는 일반적으로 롤링 업그레이드 전략이 사용됩니다. 그러나 런타임 환경의 스케줄링 전략은 서비스 프로세스를 내부 상태를 고려하지 않고 블랙박스로 취급합니다. 따라서 서비스 프로세스는 런타임 환경의 스케줄링 작업을 사전에 감지하고 종료하기 전에 필요한 정리 작업을 수행해야 합니다.
오늘은 Node.js 프로세스를 종료시킬 수 있는 다양한 시나리오를 요약하고 이러한 프로세스 종료 이벤트를 수신하여 수행할 수 있는 작업에 대해 논의하겠습니다.
원칙
프로세스는 다음 두 가지 방법 중 하나로 종료됩니다.
- 프로세스가 자발적으로 종료됩니다.
- 프로세스가 종료하도록 지시하는 시스템 신호를 수신합니다.
시스템 신호를 통한 종료
Node.js 공식 문서에는 일반적인 시스템 신호가 나열되어 있습니다. 다음 사항에 중점을 둡니다.
- SIGHUP:
Ctrl + C
를 사용하여 프로세스를 중지하는 대신 터미널이 직접 닫힐 때 트리거됩니다. - SIGINT:
Ctrl + C
를 눌러 프로세스를 중지할 때 트리거됩니다. PM2는 또한 PM2가 다시 시작하거나 중지할 때 이 신호를 하위 프로세스로 보냅니다. - SIGTERM: 일반적으로 프로세스를 정상적으로 종료하는 데 사용됩니다. 예를 들어 Kubernetes가 포드를 삭제하면 SIGTERM 신호를 보내 포드가 시간 제한 기간(기본값: 30초) 내에 정리 작업을 수행할 수 있도록 합니다.
- SIGBREAK:
Ctrl + Break
를 누르면 Windows 시스템에서 트리거됩니다. - SIGKILL: 프로세스가 즉시 종료되도록 강제하여 정리 작업을 방지합니다.
kill -9 pid
를 실행하면 프로세스가 이 신호를 받습니다. Kubernetes에서 포드가 30초 제한 시간 내에 종료되지 않으면 Kubernetes는 SIGKILL 신호를 보내 즉시 종료합니다. 마찬가지로 PM2는 다시 시작 또는 종료 중에 프로세스가 1.6초 내에 종료되지 않으면 SIGKILL을 보냅니다.
강제 종료 신호가 아닌 경우 Node.js 프로세스는 이러한 신호를 수신하고 사용자 지정 종료 동작을 정의할 수 있습니다. 예를 들어 작업을 실행하는 데 시간이 오래 걸리는 CLI 도구가 있는 경우 Ctrl + C
를 누르면 종료하기 전에 사용자에게 메시지를 표시할 수 있습니다.
const readline = require('readline'); process.on('SIGINT', () => { // readline을 사용한 간단한 명령줄 상호 작용 const rl = readline.createInterface({ input: process.stdin, output: process.stdout, }); rl.question('작업이 아직 완료되지 않았습니다. 종료하시겠습니까? ', (answer) => { if (answer === 'yes') { console.log('작업이 중단되었습니다. 프로세스를 종료합니다.'); process.exit(0); } else { console.log('작업을 계속합니다...'); } rl.close(); }); }); // 완료하는 데 1분 걸리는 작업을 시뮬레이션합니다. const longTimeTask = () => { console.log('작업이 시작되었습니다...'); setTimeout(() => { console.log('작업이 완료되었습니다.'); }, 1000 * 60); }; longTimeTask();
이 스크립트는 Ctrl + C
를 누를 때마다 프롬프트를 표시합니다.
작업이 아직 완료되지 않았습니다. 종료하시겠습니까? 아니오
작업을 계속합니다...
작업이 아직 완료되지 않았습니다. 종료하시겠습니까? 아니오
작업을 계속합니다...
작업이 아직 완료되지 않았습니다. 종료하시겠습니까? 예
작업이 중단되었습니다. 프로세스를 종료합니다.
자발적인 프로세스 종료
Node.js 프로세스는 다음 시나리오로 인해 자발적으로 종료될 수 있습니다.
- 실행 중에 catch되지 않은 오류가 발생합니다(
process.on('uncaughtException')
을 사용하여 캡처할 수 있음). - 발생한 처리되지 않은 Promise 거부 (Node.js v16부터 처리되지 않은 거부로 인해 프로세스가 종료됩니다.
process.on('unhandledRejection')
을 사용하여 처리). EventEmitter
에서error
이벤트가 발생했지만 처리되지 않았습니다.- 프로세스가 명시적으로
process.exit()
를 호출합니다. - Node.js 이벤트 루프가 비어 있습니다(즉, 대기 중인 작업이 없음). 이는
process.on('exit')
를 사용하여 감지할 수 있습니다.
PM2에는 서비스가 충돌하면 서비스를 다시 시작하는 데몬 프로세서가 있습니다. 클러스터 모듈을 사용하여 Node.js에서 유사한 자체 복구 메커니즘을 구현할 수 있습니다. 여기서 작업자 프로세스는 충돌하는 경우 자동으로 다시 시작됩니다.
const cluster = require('cluster'); const http = require('http'); const numCPUs = require('os').cpus().length; const process = require('process'); if (cluster.isMaster) { console.log(`마스터 프로세스가 시작되었습니다: ${process.pid}`); // CPU 코어 수에 따라 작업자 프로세스를 만듭니다. for (let i = 0; i < numCPUs; i++) { cluster.fork(); } // 작업자 종료 이벤트를 수신합니다. cluster.on('exit', (worker, code, signal) => { console.log(`작업자 ${worker.process.pid}가 코드: ${code || signal}로 종료되었습니다. 다시 시작하는 중...`); cluster.fork(); }); } if (cluster.isWorker) { process.on('uncaughtException', (error) => { console.log(`작업자 ${process.pid}에 오류가 발생했습니다.`, error); process.emit('disconnect'); process.exit(1); }); // HTTP 서버를 만듭니다. http .createServer((req, res) => { res.writeHead(200); res.end('Hello world\n'); }) .listen(8000); console.log(`작업자 프로세스가 시작되었습니다: ${process.pid}`); }
실제 구현
이제 Node.js 프로세스가 종료될 수 있는 다양한 시나리오를 분석했으므로 사용자가 사용자 지정 종료 동작을 정의할 수 있도록 프로세스 종료 수신기를 구현해 보겠습니다.
// exit-hook.js const tasks = []; const addExitTask = (fn) => tasks.push(fn); const handleExit = (code, error) => { // 구현 세부 사항은 아래에서 설명합니다. }; process.on('exit', (code) => handleExit(code)); process.on('SIGHUP', () => handleExit(128 + 1)); process.on('SIGINT', () => handleExit(128 + 2)); process.on('SIGTERM', () => handleExit(128 + 15)); process.on('SIGBREAK', () => handleExit(128 + 21)); process.on('uncaughtException', (error) => handleExit(1, error)); process.on('unhandledRejection', (error) => handleExit(1, error));
handleExit
의 경우 process.nextTick()
을 사용하여 동기 및 비동기 작업을 모두 적절하게 처리하는지 확인합니다.
let isExiting = false; const handleExit = (code, error) => { if (isExiting) return; isExiting = true; let hasDoExit = false; const doExit = () => { if (hasDoExit) return; hasDoExit = true; process.nextTick(() => process.exit(code)); }; let asyncTaskCount = 0; let asyncTaskCallback = () => { process.nextTick(() => { asyncTaskCount--; if (asyncTaskCount === 0) doExit(); }); }; tasks.forEach((taskFn) => { if (taskFn.length > 1) { asyncTaskCount++; taskFn(error, asyncTaskCallback); } else { taskFn(error); } }); if (asyncTaskCount > 0) { setTimeout(() => doExit(), 10 * 1000); } else { doExit(); } };
정상적인 프로세스 종료
웹 서버를 다시 시작하거나 런타임 컨테이너 스케줄링(PM2, Docker 등)을 처리할 때는 다음을 수행하는 것이 좋습니다.
- 진행 중인 요청을 완료합니다.
- 데이터베이스 연결을 정리합니다.
- 오류를 기록하고 경고를 트리거합니다.
- 기타 필요한 종료 작업을 수행합니다.
exit-hook 도구를 사용합니다.
const http = require('http'); const server = http .createServer((req, res) => { res.writeHead(200); res.end('Hello world\n'); }) .listen(8000); addExitTask((error, callback) => { console.log('오류로 인해 프로세스가 종료됩니다:', error); server.close(() => { console.log('새 요청 수락을 중단했습니다.'); setTimeout(callback, 5000); }); });
결론
Node.js 프로세스를 종료시키는 다양한 시나리오를 이해함으로써 비정상적이거나 예약된 종료를 사전에 감지하고 처리할 수 있습니다. Kubernetes 및 PM2와 같은 도구는 충돌한 프로세스를 다시 시작할 수 있지만 코드 내 모니터링을 구현하면 문제를 더 빨리 감지하고 해결할 수 있습니다.
Leapcell은 Node.js 프로젝트 호스팅을 위한 최고의 선택입니다.
Leapcell은 웹 호스팅, 비동기 작업 및 Redis를 위한 차세대 서버리스 플랫폼입니다.
다국어 지원
- Node.js, Python, Go 또는 Rust로 개발합니다.
무료로 무제한 프로젝트 배포
- 사용량에 대해서만 지불합니다. 요청, 요금이 없습니다.
탁월한 비용 효율성
- 유휴 요금 없이 사용한 만큼 지불합니다.
- 예: 평균 응답 시간 60ms에서 25달러로 694만 건의 요청을 지원합니다.
간소화된 개발자 환경
- 간편한 설정을 위한 직관적인 UI.
- 완전 자동화된 CI/CD 파이프라인 및 GitOps 통합.
- 실행 가능한 통찰력을 위한 실시간 메트릭 및 로깅.
손쉬운 확장성과 고성능
- 고도의 동시성을 쉽게 처리할 수 있도록 자동 크기 조정.
- 운영 오버헤드가 제로이므로 구축에만 집중하십시오.
설명서에서 자세히 알아보세요!
X에서 팔로우하세요: @LeapcellHQ